/*
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package eu.fastl.dragster.log.service.impl;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import eu.fastl.dragster.log.config.ColumnConfig;
import eu.fastl.dragster.log.config.ExclusionConfig;
import eu.fastl.dragster.log.config.SheetConfig;
import eu.fastl.dragster.log.config.XlsExportConfig;
import eu.fastl.dragster.log.config.XlsExportConfigReader;
import eu.fastl.dragster.log.service.LogParserResult;
import eu.fastl.dragster.log.service.LogParserService;
import eu.fastl.dragster.log.service.XlsExportService;
import eu.fastl.dragster.model.ExecutionRecord;
import eu.fastl.dragster.util.PoiUtils;

public class XlsExportServiceSimplImpl implements XlsExportService {

    private static final String AVERAGE_FUNCTION_NAME = "AVERAGE";
    private static final String MIN_FUNCTION_NAME = "MIN";
    private static final String MAX_FUNCTION_NAME = "MAX";

    private static final Logger LOGGER = LoggerFactory.getLogger(XlsExportServiceSimplImpl.class);

    private XlsExportConfigReader xlsExportConfigReader;

    private LogParserService logParserService;

    public void setXlsExportConfigReader(XlsExportConfigReader xlsExportConfigReader) {
        this.xlsExportConfigReader = xlsExportConfigReader;
    }

    public void setLogParserService(LogParserService logParserService) {
        this.logParserService = logParserService;
    }

    public void exportToSpreadSheet(String inputFileName, String outputFileName, String configFileName)
            throws IOException {
        XlsExportConfig config = null;
        if (StringUtils.isNotBlank(configFileName)) {
            config = this.xlsExportConfigReader.readConfigFromFile(configFileName);
        }

        LOGGER.info("parsing input file:" + inputFileName);

        LogParserResult parserResult = this.logParserService.parseFile(new File(inputFileName));

        List sortedIds = new ArrayList<Object>(parserResult.getExecutionRecordsById().keySet());

        // TODO: change to Comparable ?
        Collections.sort(sortedIds);
        List<String> sortedTypes = new ArrayList<String>(parserResult.getExecutionRecordsByType().keySet());
        Collections.sort(sortedTypes);
        Workbook wb;
        if (config == null) {
            wb = createDefaultWorkbook(parserResult, sortedIds, sortedTypes);
        } else {
            wb = createWorkbook(parserResult, sortedIds, config);
        }

        // allow garbage collection to free memory before writing workbook

        parserResult = null;
        sortedIds = null;
        sortedTypes = null;

        writeOutputFile(outputFileName, wb);
    }

    private Workbook createWorkbook(LogParserResult parserResult, List<?> sortedIds, XlsExportConfig config) {
        LOGGER.info("Creating Workbook...");

        Workbook wb = new XSSFWorkbook();

        for (SheetConfig sheetConfig : config.getSheets()) {
            LOGGER.debug("Writing sheet:" + sheetConfig.getName());
            Sheet sheet = wb.createSheet(sheetConfig.getName());
            int rowNum = 0;
            // header
            writeHeaderRow(sheet, sheetConfig, rowNum++);

            LOGGER.debug("Writing Table Data");
            for (Object id : sortedIds) {
                List<ExecutionRecord> executionRecords = parserResult.getExecutionRecordsById().get(id);
                if (isIncluded(sheetConfig, executionRecords, id)) {
                    if ((rowNum % 5000) == 0) {
                        LOGGER.info("Processed lines:" + rowNum);
                    }
                    Row currentRow = sheet.createRow(rowNum++);
                    int colNum = 0;
                    for (ColumnConfig columnConfig : sheetConfig.getColumns()) {
                        Cell currentCell = currentRow.createCell(colNum++);
                        if ("ID".equals(columnConfig.getType())) {
                            currentCell.setCellValue(id.toString());
                        } else if ("DETAIL".equals(columnConfig.getType())) {
                            ExecutionRecord record = getExecutionRecordForColumn(executionRecords, columnConfig);
                            if (record != null)
                                currentCell.setCellValue(record.getDetail());
                        } else if ("EXECUTION".equals(columnConfig.getType())) {
                            ExecutionRecord record = getExecutionRecordForColumn(executionRecords, columnConfig);
                            if (record != null)
                                currentCell.setCellValue(record.getExecutionTime());
                        } else if ("FORMULA".equals(columnConfig.getType())) {
                            String formula = sheetConfig.getFormulaAsString(columnConfig, rowNum);
                            currentCell.setCellFormula(formula);
                        }
                    }
                }
            }
            int lastDataRowNum = rowNum;
            writeStatsRow(AVERAGE_FUNCTION_NAME, sheet, sheetConfig, rowNum++, lastDataRowNum);
            writeStatsRow(MIN_FUNCTION_NAME, sheet, sheetConfig, rowNum++, lastDataRowNum);
            writeStatsRow(MAX_FUNCTION_NAME, sheet, sheetConfig, rowNum++, lastDataRowNum);

            // footer
            writeHeaderRow(sheet, sheetConfig, rowNum++);
        }

        return wb;
    }

    private void writeStatsRow(String functionName, Sheet sheet, SheetConfig sheetConfig, int currentRowNum,
            int lastDataRowNum) {
        LOGGER.debug("Writing Statistic Row: " + functionName);
        int colNum = 0;
        Cell currentCell = null;
        CellStyle headerCellStyle = createHeaderCellStyle(sheet);

        Row headerRow = sheet.createRow(currentRowNum);
        for (ColumnConfig columnConfig : sheetConfig.getColumns()) {
            currentCell = headerRow.createCell(colNum++);
            currentCell.setCellStyle(headerCellStyle);
            if ("ID".equals(columnConfig.getType())) {
                currentCell.setCellValue(functionName);
                currentCell.setCellStyle(headerCellStyle);
            } else if ("EXECUTION".equals(columnConfig.getType()) || "FORMULA".equals(columnConfig.getType())) {
                StringBuilder formula = new StringBuilder(functionName);
                formula.append("(");
                formula.append(PoiUtils.getColumnName(colNum - 1));
                formula.append("2");
                formula.append(":");
                formula.append(PoiUtils.getColumnName(colNum - 1));
                formula.append(lastDataRowNum);
                formula.append(")");
                currentCell.setCellFormula(formula.toString());
            }
        }
    }

    private ExecutionRecord getExecutionRecordForColumn(List<ExecutionRecord> executionRecords,
            ColumnConfig columnConfig) {
        ExecutionRecord retval = null;
        for (ExecutionRecord executionRecord : executionRecords) {
            if (columnConfig.getContainedExecutionType().equals(executionRecord.getType())) {
                if (columnConfig.getContainedDetailContains() == null
                        || executionRecord.getDetail().contains(columnConfig.getContainedDetailContains())) {
                    if (retval != null) {
                        throw new IllegalStateException("More than one executionRecord found with ID: "
                                + executionRecord.getUniqueId() + " for column config " + columnConfig
                                + " You may have to distinguish records by using EXECUTION_DETAIL_CONTAINS");
                    }
                    retval = executionRecord;
                }
            }
        }
        return retval;
    }

    private boolean isIncluded(SheetConfig sheetConfig, List<ExecutionRecord> executionRecords, Object id) {
        for (ExclusionConfig exclusionConfig : sheetConfig.getExclusions()) {
            if ("TYPE_EXISTS".equals(exclusionConfig.getType())) {
                for (ExecutionRecord executionRecord : executionRecords) {
                    if (exclusionConfig.getValue().equals(executionRecord.getType())) {
                        return false;
                    }
                }
            }
        }
        // Check for duplicates
        for (ColumnConfig columnConfig : sheetConfig.getColumns()) {
            boolean found = false;
            boolean duplicate = false;
            for (ExecutionRecord executionRecord : executionRecords) {
                if (("DETIAL".equals(columnConfig.getType())||"EXECUTION".equals(columnConfig.getType()))  &&
                        columnConfig.getContainedExecutionType().equals(executionRecord.getType())) {
                    if (columnConfig.getContainedDetailContains() == null
                            || executionRecord.getDetail().contains(columnConfig.getContainedDetailContains())) {
                        if (found) {
                            duplicate = true;
                        }
                        found = true;
                    }
                }
            }
            if (duplicate) {
                LOGGER.warn("ID contains duplicates and was ommited:" + id);
                return false;
            }
            if(columnConfig.isRequired() && ! found){
                LOGGER.warn("Ommiting ID:" + id + " because value for required column is missing:" + columnConfig);
                return false;
            }
        }
        return true;
    }

    private void writeHeaderRow(Sheet sheet, SheetConfig sheetConfig, int rowNum) {
        LOGGER.debug("Writing Header...");
        int colNum = 0;
        Cell currentCell = null;
        CellStyle headerCellStyle = createHeaderCellStyle(sheet);

        Row headerRow = sheet.createRow(rowNum);
        for (ColumnConfig columnConfig : sheetConfig.getColumns()) {
            currentCell = headerRow.createCell(colNum++);
            currentCell.setCellStyle(headerCellStyle);
            if ("ID".equals(columnConfig.getType())) {
                String title = StringUtils.defaultIfBlank(columnConfig.getTitle(), "ID");
                currentCell.setCellValue(title);
            } else if ("DETAIL".equals(columnConfig.getType())) {
                String title = StringUtils.defaultIfBlank(columnConfig.getTitle(), "Detail");
                currentCell.setCellValue(title);
            } else if ("EXECUTION".equals(columnConfig.getType())) {
                String typeName = columnConfig.getContainedExecutionType();
                String title = StringUtils.defaultIfBlank(columnConfig.getTitle(), typeName);
                currentCell.setCellValue(title);
            } else if ("FORMULA".equals(columnConfig.getType())) {
                String title = StringUtils.defaultIfBlank(columnConfig.getTitle(), "Formula");
                currentCell.setCellValue(title);
            }
        }

    }

    private CellStyle createHeaderCellStyle(Sheet sheet) {
        CellStyle headerCellStyle = sheet.getWorkbook().createCellStyle();
        headerCellStyle.setAlignment(CellStyle.ALIGN_CENTER);
        headerCellStyle.setFillBackgroundColor(IndexedColors.LIGHT_GREEN.getIndex());
        headerCellStyle.setFillPattern((short) FillPatternType.NO_FILL.ordinal());
        Font headerFont = sheet.getWorkbook().createFont();
        headerFont.setBoldweight(Font.BOLDWEIGHT_BOLD);
        headerCellStyle.setFont(headerFont);
        return headerCellStyle;
    }

    private Workbook createDefaultWorkbook(LogParserResult parserResult, List<?> sortedIds, List<String> sortedTypes) {
        LOGGER.info("Creating Workbook...");

        Workbook wb = new XSSFWorkbook();

        Sheet sheet = wb.createSheet("performance");

        LOGGER.debug("Writing Header...");

        int rowNum = writeDefaultHeader(sortedTypes, sheet);

        LOGGER.info("Writing Table Data...");

        rowNum = writeDefaultTableData(parserResult, sortedIds, sortedTypes, sheet, rowNum);

        LOGGER.info("Writing Statistics...");

        int lastDataRowNum = rowNum;

        rowNum = writeDefaultAverageRow(sortedTypes, sheet, rowNum, lastDataRowNum);

        rowNum = writeDefaultMinRow(sortedTypes, sheet, rowNum, lastDataRowNum);

        rowNum = writeDefaultMaxRow(sortedTypes, sheet, rowNum, lastDataRowNum);

        writeDefaultFooterRow(sortedTypes, sheet, rowNum);
        return wb;
    }

    private void writeDefaultFooterRow(List<String> sortedTypes, Sheet sheet, int rowNum) {
        int colNum;
        Cell currentCell;
        Row footerRow = sheet.createRow(rowNum++);
        colNum = 0;
        currentCell = footerRow.createCell(colNum++);
        currentCell.setCellValue("ID");
        for (String type : sortedTypes) {
            currentCell = footerRow.createCell(colNum++);
            currentCell.setCellValue(type);
        }
    }

    private int writeDefaultMaxRow(List<String> sortedTypes, Sheet sheet, int rowNum, int lastDataRowNum) {
        int colNum;
        Cell currentCell;
        Row maxRow = sheet.createRow(rowNum++);
        colNum = 0;
        currentCell = maxRow.createCell(colNum++);
        currentCell.setCellValue("Maximum");
        for (@SuppressWarnings("unused")
        String type : sortedTypes) {
            currentCell = maxRow.createCell(colNum++);
            StringBuilder formula = new StringBuilder(MAX_FUNCTION_NAME);
            formula.append("(");
            formula.append(PoiUtils.getColumnName(colNum - 1));
            formula.append("2");
            formula.append(":");
            formula.append(PoiUtils.getColumnName(colNum - 1));
            formula.append(lastDataRowNum);
            formula.append(")");
            currentCell.setCellFormula(formula.toString());
        }
        return rowNum;
    }

    private int writeDefaultMinRow(List<String> sortedTypes, Sheet sheet, int rowNum, int lastDataRowNum) {
        int colNum;
        Cell currentCell;
        Row minRow = sheet.createRow(rowNum++);
        colNum = 0;
        currentCell = minRow.createCell(colNum++);
        currentCell.setCellValue("Minimum");
        for (@SuppressWarnings("unused")
        String type : sortedTypes) {
            currentCell = minRow.createCell(colNum++);
            StringBuilder formula = new StringBuilder(MIN_FUNCTION_NAME);
            formula.append("(");
            formula.append(PoiUtils.getColumnName(colNum - 1));
            formula.append("2");
            formula.append(":");
            formula.append(PoiUtils.getColumnName(colNum - 1));
            formula.append(lastDataRowNum);
            formula.append(")");
            currentCell.setCellFormula(formula.toString());
        }
        return rowNum;
    }

    private int writeDefaultAverageRow(List<String> sortedTypes, Sheet sheet, int rowNum, int lastDataRowNum) {
        int colNum;
        Cell currentCell;
        Row avgRow = sheet.createRow(rowNum++);
        colNum = 0;
        currentCell = avgRow.createCell(colNum++);
        currentCell.setCellValue("Average");
        for (@SuppressWarnings("unused")
        String type : sortedTypes) {
            currentCell = avgRow.createCell(colNum++);
            StringBuilder formula = new StringBuilder(AVERAGE_FUNCTION_NAME);
            formula.append("(");
            formula.append(PoiUtils.getColumnName(colNum - 1));
            formula.append("2");
            formula.append(":");
            formula.append(PoiUtils.getColumnName(colNum - 1));
            formula.append(lastDataRowNum);
            formula.append(")");
            currentCell.setCellFormula(formula.toString());
        }
        return rowNum;
    }

    private int writeDefaultTableData(LogParserResult parserResult, List<?> sortedIds, List<String> sortedTypes,
            Sheet sheet, int rowNum) {
        int colNum;
        Cell currentCell;
        for (Object id : sortedIds) {
            if ((rowNum % 5000) == 0) {
                LOGGER.info("Processed lines:" + rowNum);
            }
            Row currentRow = sheet.createRow(rowNum++);
            List<ExecutionRecord> executionRecords = parserResult.getExecutionRecordsById().get(id);
            colNum = 0;
            Cell idCell = currentRow.createCell(colNum++);
            idCell.setCellValue(executionRecords.get(0).getUniqueId().toString());
            for (String type : sortedTypes) {
                currentCell = currentRow.createCell(colNum++);
                double executionTimeForType = getExecutionTimeForType(executionRecords, type);
                if (!Double.isNaN(executionTimeForType)) {
                    currentCell.setCellValue(executionTimeForType);
                }
            }
        }
        return rowNum;
    }

    private int writeDefaultHeader(List<String> sortedTypes, Sheet sheet) {
        int rowNum = 0;
        Row headerRow = sheet.createRow(rowNum++);
        int colNum = 0;
        Cell currentCell = headerRow.createCell(colNum++);
        currentCell.setCellValue("ID");
        for (String type : sortedTypes) {
            currentCell = headerRow.createCell(colNum++);
            currentCell.setCellValue(type);
        }
        currentCell = headerRow.createCell(colNum++);
        currentCell.setCellValue("Detail");
        return rowNum;
    }

    private double getExecutionTimeForType(List<ExecutionRecord> executionRecords, String type) {
        for (ExecutionRecord executionRecord : executionRecords) {
            if (type.equals(executionRecord.getType())) {
                // TODO: multiple records of one type per ID?
                return executionRecord.getExecutionTime();
            }
        }
        return Double.NaN;
    }

    private void writeOutputFile(String outputFileName, Workbook wb) throws FileNotFoundException, IOException {
        LOGGER.info("Writing output to file:" + outputFileName);
        FileOutputStream fileOut = new FileOutputStream(outputFileName);
        wb.write(fileOut);
        fileOut.close();
        LOGGER.info("FINISHED writing:" + outputFileName);
    }

}
